feat: add opt-in crash reporting with MetricKit (#472)#629
feat: add opt-in crash reporting with MetricKit (#472)#629
Conversation
✅ Code Coverage: 71.4%Threshold: 70% Coverage is above the minimum threshold. Generated by CI — see job summary for detailed file-level breakdown. |
…llback (#472) - MetricKit (MXCrashDiagnostic) as primary crash diagnostic source - POSIX signal handler fallback (strictly async-signal-safe: only write/_exit) - Opt-in dialog on first launch with privacy explanation - Toggle in File menu to enable/disable at any time - CrashReport model with Codable serialization - CrashReportStore persists reports to Application Support - Localization for all 9 languages (de, en, es, fr, ja, ko, pt-BR, ru, zh-Hans) - 32 unit tests covering settings, model, store, parseCallStack, edge cases
…action, JSON callstack parsing
f467098 to
4af84a3
Compare
Code Review: feat: add opt-in crash reporting with MetricKit (#472)Вердикт: REQUEST CHANGES (не могу поставить формально, т.к. свой PR) Билд и SwiftLint прошли. Архитектура в целом грамотная: разделение на Settings/Store/Manager/View правильное, signal handler async-signal-safe, opt-in flow на месте. Но есть критические проблемы. CRITICAL — Обязательно исправить1. Данные НЕ отправляются, но UI говорит пользователю что они отправляются Локализованные строки во всех 9 языках говорят: "Pine can send anonymous crash reports". Но в коде нет НИКАКОЙ сетевой отправки — ни URLSession, ни endpoint, ни API-вызовов. Crash reports складываются в Это либо:
В обоих случаях нельзя мержить. Либо убрать слово "send" из всех локализаций и переформулировать как "Pine can collect crash reports locally to help diagnose issues", либо реализовать отправку. 2.
3.
Но 4. Signal handler — memory leak let cString = strdup(path)
crashMarkerCString = cString
IMPORTANT — Нужно исправить5.
6. Opt-in dialog показывается в КАЖДОМ окне проекта
7.
8. Нет UI для просмотра/экспорта crash reports Crash reports складываются на диск, но пользователь не может их увидеть, экспортировать или удалить через UI. Issue #472 предлагал "send to GitHub Issues automatically or via email". Текущая реализация — мёртвое хранилище данных. Как минимум нужен пункт меню "View Crash Reports" или "Export Crash Report". 9. Нет теста на pruning edge case Тест 10. Локализация opt-in кнопок — hardcoded В Button(String(localized: "crashReporting.optIn.disable")) { ... }
Button(String(localized: "crashReporting.optIn.enable")) { ... }Остальные строки идут через SUGGESTIONS — Желательно11. Иконка Для crash reporting лучше 12. Просто split по newlines + trim. Название misleading — это не "parsing", а "splitting". Либо переименовать в 13. Privacy note говорит "open tab count", но В Итого4 критических, 6 важных, 3 предложения. Главная проблема: фича обещает пользователю отправку crash reports, но ничего не отправляет. Thread safety отсутствует в Store и Manager. Отправить на доработку. |
…text, race condition - CrashReportStore: add serial DispatchQueue for all disk I/O (thread safety) - CrashReportStore: add revealInFinder() and copyAllToClipboard() export methods - CrashReportingManager.stop(): restore default signal handlers + free strdup'd C string to prevent memory leak on toggle cycles - CrashReportingManager.installSignalHandlers(): free previous strdup on re-install - ContentView+Helpers: set hasShownPrompt immediately before async delay to prevent opt-in dialog showing in multiple windows simultaneously - CrashReportingOptInView: use Strings constants instead of inline String(localized:) - Localizable.xcstrings: replace misleading "send" language with "collect locally" / "stored locally" across all 9 locales (en, de, es, fr, ja, ko, pt-BR, ru, zh-Hans) - Strings.swift: add crashReportingOptInEnable/Disable constants - Tests: add thread safety, export, race condition, stop(), and strings tests
batonogov
left a comment
There was a problem hiding this comment.
Повторное ревью PR #629
Статус предыдущих замечаний
- UI обман "send" — ИСПРАВЛЕНО. Тексты теперь "collect locally" / "stored locally". Методы
revealInFinder()иcopyAllToClipboard()реализованы вCrashReportStore. - Thread safety CrashReportStore — ИСПРАВЛЕНО. Serial
DispatchQueue, все публичные методы черезqueue.sync. - Memory leak в signal handler — ИСПРАВЛЕНО.
stop()вызываетfree()+SIG_DFL.installSignalHandlers()освобождает предыдущийstrdup. - Opt-in dialog race — ИСПРАВЛЕНО.
hasShownPrompt = trueстоит ДОasyncAfter. - Hardcoded strings — ИСПРАВЛЕНО. Все строки в
Strings.swift. - "send" в Localizable.xcstrings — ИСПРАВЛЕНО.
Новые проблемы
Critical (must fix)
C1. SwiftLint failure — CI красный.
PineTests/CrashReportingTests.swift:646 — empty_count violation. Переменная count сравнивается с 0:
let count = store.copyAllToClipboard()
#expect(count == 0)SwiftLint трактует это как нарушение. Переименовать переменную:
let copiedCount = store.copyAllToClipboard()
#expect(copiedCount == 0)Аналогично проверить остальные вхождения count == / count != в этом файле.
C2. revealInFinder() и copyAllToClipboard() — мёртвый код.
Эти методы реализованы в CrashReportStore, но НЕ подключены ни к какому UI-элементу. Пользователь не может до них добраться. Замечание #1 требовало добавить их в UI (меню или кнопки). Сейчас это мёртвый код — нужно добавить пункты меню в PineApp.swift (например в секцию File или рядом с toggle crash reporting), либо добавить кнопки в CrashReportingOptInView/Settings.
Important (should fix)
I1. Signal handler использует Swift runtime.
signalHandler() — Swift function, использующая guard let, withUnsafePointer, var/let — всё это может вызывать Swift runtime, что не является async-signal-safe. Для fallback-маркера это допустимый компромисс, но стоит как минимум добавить @convention(c) аннотацию и комментарий, что это best-effort fallback (а не строго async-signal-safe).
I2. CrashReportingManager не имеет thread safety гарантий.
startIfEnabled() и stop() обращаются к глобальной crashMarkerCString без синхронизации. На практике оба вызываются с main thread, но это нигде не задокументировано и не enforced. Добавить @MainActor на CrashReportingManager или хотя бы dispatchPrecondition(condition: .onQueue(.main)) в start/stop.
Suggestions (nice to have)
S1. CrashReportStore.save() использует queue.sync — может заблокировать вызывающий поток.
Если save() вызывается из MetricKit callback (произвольный поток), queue.sync блокирует этот поток до завершения I/O. Лучше queue.async для save() — fire and forget, ведь результат не нужен вызывающему.
S2. Тесты withCleanDefaults дублируют код.
CrashReportingSettingsTests и CrashReportingSettingsRaceTests содержат идентичный withCleanDefaults(). Вынести в общий helper.
…, SwiftLint - Rename count to copiedCount in tests to fix SwiftLint empty_count - Add menu buttons for Reveal Crash Reports in Finder and Copy Crash Reports so revealInFinder()/copyAllToClipboard() are no longer dead code - Replace bare crashMarkerCString global with CrashMarkerStorage struct using os_unfair_lock for thread-safe access (trylock in signal handler) - Rewrite signal handler to avoid Swift runtime: no guard/let unwrap, no withUnsafePointer closures, byte-by-byte POSIX write() - Add tests for new menu icons
Code Review — Раунд 3Все 4 замечания раунда 2 исправлены ✓ Новые проблемыImportant:
Suggestions:
|
…r docs - Replace loadAll().isEmpty with lightweight isEmpty property (dir listing only, no JSON decoding) to avoid disk I/O on every menu render - Add missing 7 translations (de, es, fr, ja, ko, pt-BR, zh-Hans) for menu.crashReports.copy and menu.crashReports.reveal in xcstrings - Fix misleading signal handler comment — guard let on raw pointer is safe - Add warning log when strdup returns nil (OOM) - Add isEmpty test for CrashReportStore
Code Review — Раунд 4Все 4 замечания раунда 3 исправлены ✓ Новая проблемаImportant: Toggle через меню в Fix: одна строка |
Code Review -- Round 5 (final)Прошёлся по полному diff (12 файлов, ~2100 строк). Все замечания с раундов 1-4 исправлены. Что сделано хорошо
Critical / ImportantНет. Все ранее найденные проблемы исправлены. ИтогоPR готов к мерджу (после прохождения CI). |
Closes #472 - MetricKit + signal handler fallback, opt-in dialog, menu toggle, 32 tests, 9 languages